﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;

namespace Authentication.Data.DataContextStorage
{
    public class DbContextManager
    {
        /// <summary>
        ///     Maintains a dictionary of db context builders, one per database.  The key is a
        ///     connection string name used to look up the associated database, and used to decorate respective
        ///     repositories. If only one database is being used, this dictionary contains a single
        ///     factory with a key of <see cref="DefaultConnectionStringName" />.
        /// </summary>
        private static readonly Dictionary<string, IDbContextBuilder<DbContext>> _dbContextBuilders =
            new Dictionary<string, IDbContextBuilder<DbContext>>();

        /// <summary>
        /// The _sync lock.
        /// </summary>
        private static readonly object _syncLock = new object();

        /// <summary>
        ///     The default connection string name used if only one database is being communicated with.
        /// </summary>
        public static readonly string DefaultConnectionStringName = "LgevhEntities";

        /// <summary>
        ///     An application-specific implementation of IDbContextStorage must be setup either thru
        ///     <see cref="InitStorage" /> or one of the <see cref="Init" /> overloads.
        /// </summary>
        private static IDbContextStorage _storage { get; set; }

        /// <summary>
        ///     Used to get the current db context session if you're communicating with a single database.
        ///     When communicating with multiple databases, invoke <see cref="CurrentFor()" /> instead.
        /// </summary>
        public static DbContext Current
        {
            get { return CurrentFor(DefaultConnectionStringName); }
        }

        /// <summary>
        /// The init.
        /// </summary>
        /// <param name="mappingAssemblies">
        /// The mapping assemblies.
        /// </param>
        /// <param name="recreateDatabaseIfExist">
        /// The recreate database if exist.
        /// </param>
        /// <param name="lazyLoadingEnabled">
        /// The lazy loading enabled.
        /// </param>
        public static void Init(string[] mappingAssemblies, bool recreateDatabaseIfExist = false,
            bool lazyLoadingEnabled = true)
        {
            Init(DefaultConnectionStringName, mappingAssemblies, recreateDatabaseIfExist, lazyLoadingEnabled);
        }

        /// <summary>
        /// The init.
        /// </summary>
        /// <param name="connectionStringName">
        /// The connection string name.
        /// </param>
        /// <param name="mappingAssemblies">
        /// The mapping assemblies.
        /// </param>
        /// <param name="recreateDatabaseIfExist">
        /// The recreate database if exist.
        /// </param>
        /// <param name="lazyLoadingEnabled">
        /// The lazy loading enabled.
        /// </param>
        public static void Init(string connectionStringName, string[] mappingAssemblies,
            bool recreateDatabaseIfExist = false, bool lazyLoadingEnabled = true)
        {
            AddConfiguration(connectionStringName, mappingAssemblies, recreateDatabaseIfExist, lazyLoadingEnabled);
        }

        /// <summary>
        /// The init storage.
        /// </summary>
        /// <param name="storage">
        /// The storage.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// </exception>
        /// <exception cref="ApplicationException">
        /// </exception>
        public static void InitStorage(IDbContextStorage storage)
        {
            if (storage == null)
            {
                throw new ArgumentNullException("storage");
            }

            if ((_storage != null) && (_storage != storage))
            {
                throw new ApplicationException("A storage mechanism has already been configured for this application");
            }

            _storage = storage;
        }

        /// <summary>
        /// Used to get the current DbContext associated with a key; i.e., the key
        ///     associated with an object context for a specific database.
        ///     If you're only communicating with one database, you should call <see cref="Current"/> instead,
        ///     although you're certainly welcome to call this if you have the key available.
        /// </summary>
        /// <param name="key">
        /// The key.
        /// </param>
        /// <returns>
        /// The <see cref="DbContext"/>.
        /// </returns>
        public static DbContext CurrentFor(string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentNullException("key");
            }

            if (_storage == null)
            {
                throw new ApplicationException("An IDbContextStorage has not been initialized");
            }

            DbContext context = null;
            lock (_syncLock)
            {
                if (!_dbContextBuilders.ContainsKey(key))
                {
                    throw new ApplicationException("An DbContextBuilder does not exist with a key of " + key);
                }

                context = _storage.GetDbContextForKey(key);

                if (context == null)
                {
                    context = _dbContextBuilders[key].BuildDbContext();
                    _storage.SetDbContextForKey(key, context);
                }
            }

            return context;
        }

        /// <summary>
        ///     This method is used by application-specific db context storage implementations
        ///     and unit tests. Its job is to walk thru existing cached object context(s) and Close() each one.
        /// </summary>
        public static void CloseAllDbContexts()
        {
            foreach (var ctx in _storage.GetAllDbContexts())
            {
                if (((IObjectContextAdapter)ctx).ObjectContext.Connection.State == ConnectionState.Open)
                {
                    ((IObjectContextAdapter)ctx).ObjectContext.Connection.Close();
                }
            }
        }

        /// <summary>
        /// The add configuration.
        /// </summary>
        /// <param name="connectionStringName">
        /// The connection string name.
        /// </param>
        /// <param name="mappingAssemblies">
        /// The mapping assemblies.
        /// </param>
        /// <param name="recreateDatabaseIfExists">
        /// The recreate database if exists.
        /// </param>
        /// <param name="lazyLoadingEnabled">
        /// The lazy loading enabled.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// </exception>
        private static void AddConfiguration(string connectionStringName, string[] mappingAssemblies,
            bool recreateDatabaseIfExists = false, bool lazyLoadingEnabled = true)
        {
            if (string.IsNullOrEmpty(connectionStringName))
            {
                throw new ArgumentNullException("connectionStringName");
            }

            if (mappingAssemblies == null)
            {
                throw new ArgumentNullException("mappingAssemblies");
            }

            lock (_syncLock)
            {
                _dbContextBuilders.Add(connectionStringName,
                    new DbContextBuilder<DbContext>(connectionStringName, mappingAssemblies, recreateDatabaseIfExists,
                        lazyLoadingEnabled));
            }
        }
    }
}
